Skip to content

feat: add CraftDImage component for Android/KMP#102

Closed
rviannaoliveira wants to merge 21 commits intomainfrom
feature/add-craftd-image
Closed

feat: add CraftDImage component for Android/KMP#102
rviannaoliveira wants to merge 21 commits intomainfrom
feature/add-craftd-image

Conversation

@rviannaoliveira
Copy link
Copy Markdown
Contributor

Summary

  • Adds CraftDImage component to craftd-core, craftd-compose and craftd-xml
  • ImageProperties model with url, contentScale, contentDescription and actionProperties
  • CraftDContentScale enum mapping to Compose ContentScale
  • CraftDImageBuilder (Compose) with injectable imageLoader — not pre-registered by design
  • CraftDImageComponentRender (XML) with injectable imageLoader — registered in CraftDBuilderManager
  • Unit tests for ImageProperties serialization, toContentScale() and CraftDImageBuilder key
  • Docs updated: compose.md and view-system.md
  • Sample app demonstrating component with Coil (Compose) and Picasso (XML)
  • Context optimization: platform-specific instructions in .claude/instructions/, platform: frontmatter rule for /propose

Test plan

  • ./gradlew :craftd-core:testDebugUnitTest passes
  • ./gradlew :craftd-compose:testDebugUnitTest passes
  • ./gradlew :app-sample-android:assembleDebug passes
  • CraftDImage renders in Compose sample screen
  • CraftDImage renders in XML sample screen
  • Tap on image triggers action (deeplink/analytics)

🤖 Generated with Claude Code

rviannaoliveira and others added 21 commits April 11, 2026 17:37
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add OpenSpec commands and skills (propose, explore, apply, archive)
- Add Android/Compose skills (testing, performance, accessibility, gradle, compose-ui)
- Update CLAUDE.md with folder patterns, code principles and docs rule
- Add GitHub Copilot equivalents (prompts and skills)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PostToolUse hook that triggers Claude review after gh pr create
- Add review-pr.sh script with checklist based on CLAUDE.md rules
- Add PR review section to CLAUDE.md with review guidelines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rules derived from PR #78 review comments:
- Rule 9: every new builder must be registered in CraftDBuilderManager
- Rule 10: external library dependencies must be abstracted via interface/parameter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rule belongs in propose.md, not in project-level instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ifacts

Scoped change to Android/KMP only (iOS and Flutter out of scope).
Generated proposal.md, design.md, specs/craftd-image/spec.md and tasks.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rule 11: every new component must have a working example in the
corresponding sample app (app-sample-android for Compose and XML).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add section 6 with tasks to register and demonstrate CraftDImage
in app-sample-android for both Compose and XML setups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove PostToolUse/Edit hook and build-on-task-complete.sh.
Add explicit rule: run ./gradlew build before marking task [x].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Define parallel agent strategy in CLAUDE.md: core (sequential) →
compose+xml (parallel) → docs/sample → revisor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add IMAGE_COMPONENT to CraftDComponentKey enum
- Add CraftDContentScale enum (CROP, FIT, FILL_BOUNDS, FILL_WIDTH, FILL_HEIGHT, INSIDE, NONE)
- Add ImageProperties data class with url, contentScale, contentDescription, actionProperties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compose:
- Add toContentScale() extension mapping CraftDContentScale → ContentScale
- Add CraftDImage composable with injectable imageLoader lambda
- Add CraftDImageBuilder with injectable imageLoader constructor param

XML:
- Add CraftDImageComponent (AppCompatImageView wrapper)
- Add CraftDImageComponentRender with injectable imageLoader
- Register CraftDImageComponentRender in CraftDBuilderManager via optional imageLoader param

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Unit tests for ImageProperties serialization, toContentScale() and CraftDImageBuilder key
- Updated docs/how-to-use/compose.md and view-system.md with CraftDImage usage examples
- Registered CraftDImageBuilder (Coil) in Compose sample and CraftDImageComponentRender (Picasso) in XML sample
- Added CraftDImage entry to dynamic.json
- Switched app-sample-android to local craftd-xml project dependency
- Added Coil 2.6.0 to libs.versions.toml
- Deleted notes.md after context consumed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@rviannaoliveira rviannaoliveira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat: add CraftDImage component for Android/KMP

Resumo geral

Implementação bem estruturada no geral — separação de plataformas respeitada, abstração de imageLoader correta, sample funcional, docs atualizados. Porém há um problema de comportamento que precisa ser corrigido antes do merge e alguns pontos menores.


🔴 Bloqueador — contentScale definido no model mas nunca utilizado

ImageProperties.contentScale existe no modelo e é documentado, mas não é passado em nenhuma das implementações:

Compose (CraftDImage.kt): o imageLoader recebe (url, contentDescription, modifier) — sem ContentScale. Há até uma função toContentScale() criada em UtilsCompose.kt, mas ela não é chamada em lugar nenhum no composable.

XML (CraftDImageComponentRender.kt): o imageLoader recebe (url, imageView)contentScale completamente ignorado.

O resultado é que o servidor pode enviar "contentScale": "CROP" e nada acontece. Correto seria incluir ContentScale na assinatura do imageLoader do Compose e um valor equivalente (ImageView.ScaleType) no XML, ou ao menos aplicar internamente quando o imageLoader não precisar controlar isso.


🟡 Atenção — url nullable com fallback silencioso

Em CraftDImage.kt:19:

imageLoader(properties.url.orEmpty(), ...)

Se url for null, chama o imageLoader com string vazia. Dependendo do imageLoader (Coil, Picasso, Glide), isso pode gerar erro em runtime ou exibir imagem errada silenciosamente. O correto é guardar contra null e não renderizar:

val url = properties.url ?: return
imageLoader(url, ...)

🟡 Atenção — CraftDImageBuilder não registrado no CraftDBuilderManager do Compose (Regra 9)

A PR descreve isso como "not pre-registered by design" — o que faz sentido dado que imageLoader é obrigatório na construção (Regra 10). Mas a Regra 9 exige registro em CraftDBuilderManager. Sugiro um de dois caminhos:

  • Documentar no CLAUDE.md que builders com dependências externas obrigatórias são exceção à Regra 9 (precisam ser registrados pelo consumidor), ou
  • Tornar imageLoader opcional com uma implementação padrão no-op para permitir registro default

Sem essa resolução, a regra fica inconsistente.


🟡 Atenção — Ausência de testes para CraftDImageComponentRender (XML)

Há testes para ImageProperties, ContentScaleExtension e CraftDImageBuilder, mas nenhum para CraftDImageComponentRender. A Regra 7 exige testes quando possível. Ao menos um teste verificando que bindView chama imageLoader com a URL correta e que o click listener é registrado quando actionProperties não é null seria suficiente.


✅ O que está correto

  • Regra 1 — Nenhuma dependência entre módulos de plataforma
  • Regra 2CraftDImageBuilder implementa CraftDBuilder; CraftDImageComponentRender implementa CraftDViewRenderer
  • Regra 3onAction coberto com fallback em ambas as plataformas (actionProperties?.let { ... })
  • Regra 6 — Nome CraftDImage consistente entre Compose e XML
  • Regra 8docs/how-to-use/compose.md e view-system.md atualizados
  • Regra 9 — XML: CraftDImageComponentRender registrado corretamente via imageLoader?.let { ... } no getBuilderRenders()
  • Regra 10imageLoader injetável, sem acoplamento direto a Coil/Picasso/Glide
  • Regra 11 — Sample demonstra o componente em Compose (Coil) e XML (Picasso)
  • Testes nomenclatura — backtick correto: `given X when Y then Z`
  • dynamic.json inclui CraftDImage com actionProperties para validar tap ✓

@github-actions
Copy link
Copy Markdown
Contributor

🦙 MegaLinter status: ⚠️ WARNING

Descriptor Linter Files Fixed Errors Warnings Elapsed time
⚠️ KOTLIN detekt yes 139 no 4.23s
⚠️ MARKDOWN markdown-table-formatter 51 1 0 0.29s
⚠️ YAML prettier 18 1 4 0.88s

See detailed report in MegaLinter reports

You could have the same capabilities but better runtime performances if you use a MegaLinter flavor:

MegaLinter is graciously provided by OX Security

Copy link
Copy Markdown
Contributor Author

@rviannaoliveira rviannaoliveira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — CraftDImage Android/KMP

✅ Aprovado com observações

Arquitetura

  • CraftDImageBuilder implementa CraftDBuilder corretamente ✓
  • CraftDImageComponentRender estende CraftDViewRenderer corretamente ✓
  • Sem dependência entre módulos de plataforma ✓
  • onAction coberto em Compose e XML ✓
  • imageLoader corretamente abstraído via lambda — Coil/Picasso nunca entram nos builders ✓
  • Builder XML registrado via parâmetro opcional imageLoader em getBuilderRenders()
  • CraftDImageBuilder Compose não pré-registrado por design (requer injeção de imageLoader) ✓

Testes

  • Nomenclatura em backtick ✓
  • ImagePropertiesTest cobre serialização/desserialização ✓
  • ContentScaleExtensionTest cobre todos os 7 valores + null ✓
  • CraftDImageBuilderTest cobre apenas a propriedade key — comportamento do imageLoader e actionProperties não testados. Limitação conhecida pela ausência de Compose UI test infra no projeto.

Docs

  • compose.md e view-system.md atualizados ✓

Observações

1. Alias no libs.versions.toml
coil_compose usa underscore mas a convenção do catalog é hífen (coil-compose), que gera o accessor libs.coil.compose de forma mais legível. Não é blocker, mas vale ajustar para consistência com os demais aliases.

2. @Stable/@Immutable em commonMain
ImageProperties e CraftDContentScale usam anotações do compose.runtime em commonMain. O Compose Multiplatform Runtime é KMP-safe, então tecnicamente não viola a regra de commonMain sem deps de plataforma. Mas vale ter ciência que isso acopla o model ao Compose — se no futuro o core precisar ser usado sem Compose, seria necessário remover essas anotações.

3. app-sample-android agora usa projects.craftdXml local
A dependência io.github.codandotv:craftd-xml:1.1.0 foi removida e substituída pelo projeto local. Correto para que o sample use a versão com imageLoader, mas vale garantir que o CI tem o ambiente para buildar o módulo local.

@rviannaoliveira
Copy link
Copy Markdown
Contributor Author

Substituído pelo PR #103 com branch limpa a partir do main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant